import {ShpType} from './index.js'
import utils from '../utils'
const NullRecord = function() {
  return {
    isNull: true,
    pointCount: 0,
    partCount: 0,
    byteLength: 12
  }
}
// Returns a constructor function for a shape record class with
//   properties and methods for reading coordinate data.
//
// Record properties
//   type, isNull, byteLength, pointCount, partCount (all types)
//
// Record methods
//   read(), readPoints() (all types)
//   readBounds(), readCoords()  (all but single point types)
//   readPartSizes() (polygon and polyline types)
//   readZBounds(), readZ() (Z types except POINTZ)
//   readMBounds(), readM(), hasM() (M and Z types, except POINT[MZ])
//
function ShpRecordClass(type) {
  const hasBounds = ShpType.hasBounds(type)
  const hasParts = ShpType.isMultiPartType(type)
  const hasZ = ShpType.isZType(type)
  const hasM = ShpType.isMType(type)
  const singlePoint = !hasBounds
  const mzRangeBytes = singlePoint ? 0 : 16

  if (type === 0) {
    return NullRecord
  }

  // @bin is a BinArray set to the first data byte of a shape record
  const constructor = function ShapeRecord(bin, bytes) {
    const pos = bin.position()
    this.id = bin.bigEndian().readUint32()
    this.type = bin.littleEndian().skipBytes(4).readUint32()
    if (this.type === 0) {
      return new NullRecord()
    }
    if (bytes > 0 !== true || (this.type != type && this.type !== 0)) {
      utils.error("Unable to read a shape -- .shp file may be corrupted");
    }
    this.byteLength = bytes // bin.readUint32() * 2 + 8; // bytes in content section + 8 header bytes
    if (singlePoint) {
      this.pointCount = 1
      this.partCount = 1
    } else {
      bin.skipBytes(32) // skip bbox
      this.partCount = hasParts ? bin.readUint32() : 1
      this.pointCount = bin.readUint32()
    }
    this._data = function() {
      return bin.position(pos)
    }
  }

  // base prototype has methods shared by all Shapefile types except NULL type
  // (Type-specific methods are mixed in below)
  const proto = {
    // return offset of [x, y] point data in the record
    _xypos: function() {
      let offs = 12 // skip header & record type
      if (!singlePoint) offs += 4 // skip point count
      if (hasBounds) offs += 32
      if (hasParts) offs += 4 * this.partCount + 4 // skip part count & index
      return offs
    },

    readCoords: function() {
      if (this.pointCount === 0) return null
      const partSizes = this.readPartSizes()
      const xy = this._data().skipBytes(this._xypos())

      return partSizes.map(function(pointCount) {
        return xy.readFloat64Array(pointCount * 2)
      })
    },

    readXY: function() {
      if (this.pointCount === 0) return new Float64Array(0)
      return this._data().skipBytes(this._xypos()).readFloat64Array(this.pointCount * 2)
    },

    readPoints: function() {
      const xy = this.readXY()
      const zz = hasZ ? this.readZ() : null
      const mm = hasM && this.hasM() ? this.readM() : null
      const points = []

      for (let i=0, n=xy.length / 2; i<n; i++) {
        let p = [xy[i*2], xy[i*2+1]]
        if (zz) p.push(zz[i])
        if (mm) p.push(mm[i])
        points.push(p)
      }
      return points
    },

    // Return an array of point counts in each part
    // Parts containing zero points are skipped (Shapefiles with zero-point
    // parts are out-of-spec but exist in the wild).
    readPartSizes: function() {
      const sizes = []
      if (this.pointCount === 0) {
        // no parts
      } else if (this.partCount == 1) {
        // single-part type or multi-part type with one part
        sizes.push(this.pointCount)
      } else {
        // more than one part
        let startId = 0
        const bin = this._data().skipBytes(56) // skip to second entry in part index
        for (let i=0, n=this.partCount; i<n; i++) {
          const partLen = (i < n - 1 ? bin.readUint32() : this.pointCount) - startId
          if (partLen > 0) {
            sizes.push(partLen)
            startId += partLen
          }
        }
      }
      return sizes
    }
  }

  const singlePointProto = {
    read: function() {
      let n = 2
      if (hasZ) n++
      if (this.hasM()) n++
      return this._data().skipBytes(12).readFloat64Array(n)
    },

    stream: function(sink) {
      const src = this._data().skipBytes(12)
      sink.addPoint(src.readFloat64(), src.readFloat64())
      sink.endPath()
    }
  }

  const multiCoordProto = {
    readBounds: function() {
      return this._data().skipBytes(12).readFloat64Array(4)
    },

    stream: function(sink) {
      const sizes = this.readPartSizes()
      const xy = this.readXY()
      let i = 0
      let j = 0
      while (i < sizes.length) {
        let n = sizes[i]
        while (n-- > 0) {
          sink.addPoint(xy[j++], xy[j++])
        }
        sink.endPath()
        i++
      }
      if (xy.length != j) utils.error('Counting error')
    },

    // TODO: consider switching to this simpler functino
    stream2: function(sink) {
      const sizes = this.readPartSizes()
      const bin = this._data().skipBytes(this._xypos())
      let i = 0
      while (i < sizes.length) {
        let n = sizes[i]
        while (n-- > 0) {
          sink.addPoint(bin.readFloat64(), bin.readFloat64())
        }
        sink.endPath()
        i++
      }
    },

    read: function() {
      const parts = []
      const sizes = this.readPartSizes()
      const points = this.readPoints()
      for (let i=0, n = sizes.length - 1; i<n; i++) {
        parts.push(points.splice(0, sizes[i]))
      }
      parts.push(points)
      return parts
    }
  }

  const mProto = {
    _mpos: function() {
      let pos = this._xypos() + this.pointCount * 16
      if (hasZ) {
        pos += this.pointCount * 8 + mzRangeBytes
      }
      return pos
    },

    readMBounds: function() {
      return this.hasM() ? this._data().skipBytes(this._mpos()).readFloat64Array(2) : null
    },

    // TODO: group into parts, like readCoords()
    readM: function() {
      return this.hasM() ? this._data().skipBytes(this._mpos() + mzRangeBytes).readFloat64Array(this.pointCount) : null
    },

    // Test if this record contains M data
    // (according to the Shapefile spec, M data is optional in a record)
    //
    hasM: function() {
      const bytesWithoutM = this._mpos()
      const bytesWithM = bytesWithoutM + this.pointCount * 8 + mzRangeBytes
      if (this.byteLength == bytesWithoutM) {
        return false
      } else if (this.byteLength == bytesWithM) {
        return true
      } else {
        utils.error("#hasM() Counting error")
      }
    }
  }

  const zProto = {
    _zpos: function() {
      return this._xypos() + this.pointCount * 16
    },

    readZBounds: function() {
      return this._data().skipBytes(this._zpos()).readFloat64Array(2)
    },

    // TODO: group into parts, like readCoords()
    readZ: function() {
      return this._data().skipBytes(this._zpos() + mzRangeBytes).readFloat64Array(this.pointCount)
    }
  }

  if (singlePoint) {
    utils.extend(proto, singlePointProto)
  } else {
    utils.extend(proto, multiCoordProto)
  }
  if (hasZ) utils.extend(proto, zProto)
  if (hasM) utils.extend(proto, mProto)

  constructor.prototype = proto
  proto.constructor = constructor
  return constructor
}

export default ShpRecordClass
